-- NormalAudienceM
-- Created by cheny3 Jun/06/2016
-- 通用观众模块

module("NormalAudienceM", package.seeall);

local PURE_FILE = "NormalAudienceM";

-- 内部函数申明
local whenMonsterDie;
local whenDungeonAready;
local whenMonsterDamaged;
local checkAudienceTalk;
local initMonster;

-- 配置表
local normalAudienceTable = {};
local audienceTalkTable = {};
local talkConfigByClass = {};

-- 观众发言队列
local audienceTalkQueue = {};
-- 观众格子最近一次发言时间
local gridsLastTalkTimeMap = {};

local damagedAudienceTempList = {};    -- 受创观众临时列表
local deadAudienceTempList    = {};    -- 死亡观众临时列表
local lastEnterTalkTime       = 0;     -- 最近一次观众入场发言的时间
local lastCombatTalkTime      = 0;     -- 最近一次观众战斗过程发言的时间
local lastWinTalkTime         = 0;     -- 最近一次观众胜利发言的时间

local TALK_CHECK_INTERVAL = 1;    -- 发言调度器执行间隔
local STAGE_TALK_INTERVAL = 3;    -- 每个阶段发言的时间间隔
local GRID_TALK_CD_TIME   = 2.5;  -- 每个格子发言冷却时间
local MAX_MONSTER_NUM     = 3;    -- 特殊怪物发言上限数量

-- 当次角斗场是否播放过胜利发言
local hasWinTalk = false;

-- 当前发言的优先级
local curTalkLevel = 1;

-- 优先级设置
local COMBAT_PROCESS_TALK  = 1;  -- 战斗过程发言的优先级
local AUDIENCE_DAMAGE_TALK = 2;  -- 观众受创发言的优先级
local PLAYER_WIN_TALK      = 3;  -- 玩家胜利发言的优先级
local AUDIENCE_DIED_TALK   = 4;  -- 观众死亡发言的优先级
local PLAYER_ENTRANCE_TALK = 5;  -- 玩家入场发言的优先级

-- 载入配置表
local _init = false;
local loadCsv = function()
    if _init then return end
    _init = true;

    -- 观众配置表
    normalAudienceTable = {};
    local normal_audience_CSV = get_normal_audience_CSV();
    for i = 1, #normal_audience_CSV do
        local id = normal_audience_CSV[i].class_id;
        normalAudienceTable[id] = normal_audience_CSV[i];
    end
    normal_audience_CSV = nil;

    -- 对白配置表
    audienceTalkTable = {};
    local normal_audience_talk_CSV = get_normal_audience_talk_CSV();
    for i = 1, #normal_audience_talk_CSV do
        local id = normal_audience_talk_CSV[i].class_id;
        audienceTalkTable[id] = normal_audience_talk_CSV[i];
    end
    normal_audience_talk_CSV = nil;

    -- 按照class整理对话
    initTalkConfig();
end

-- 模块初始化操作
function init()
    if not _init then
        loadCsv();
    end

    -- 注册所有事件
    registerEvent();

    -- 注册怪物初始化回调
    MonsterM.registerInitCallback("NormalAudienceM", initMonster);
end

-- 按照classId整理对话
function initTalkConfig()
    for _, info in pairs(audienceTalkTable) do
        local classIds = info["class_id"];
        for _, classId in pairs(classIds) do
            local stage = info["stage"];
            local config = talkConfigByClass[classId] or {};
            local stageConfig = config[stage] or {};

            table.insert(stageConfig, info);
            config[stage] = stageConfig;
            talkConfigByClass[classId] = config;
        end
    end
end

-- 获取talkConfig
function getTalkConfig()
    return talkConfigByClass;
end

-- 注册事件
function registerEvent()
    -- 反注册所有事件
    EventMgr.removeAll(PURE_FILE);

    -- 关注怪物死亡的事件
    EventMgr.register(PURE_FILE, event.MONSTER_DIE_LOGIC, whenMonsterDie);

    -- 关注怪物受创事件
    EventMgr.register(PURE_FILE, event.MONSTER_DAMAGED, whenMonsterDamaged);

    -- 关注玩家开始新一层
    EventMgr.register(PURE_FILE, event.DUNGEON_AREADY, whenDungeonAready);
end

-- 检索观众配置
function queryAudience(id, path)
    if not normalAudienceTable[id] then
        return nil;
    end

    if path == nil or path == "" then
        return table.deepcopy(normalAudienceTable[id]);
    end

    return table.deepcopy(normalAudienceTable[id][path]);
end

-- 开始新一层
whenDungeonAready = function(args)
    if not isInForcus() then
        return;
    end

    -- 重置发言记录
    resetTalkRecord();

    -- 准备入场发言
    prepareEnterTalk();

    -- 启动观众发言调度器
    ScheduleM.createScheme("NormalAudienceM", checkAudienceTalk, TALK_CHECK_INTERVAL, true)
end

-- 怪物死亡的回调
whenMonsterDie = function(args)
    if not isInForcus() then
        return;
    end

    local monster = args.monster;
    if not monster then
        return;
    end

    -- 死亡奖励
    monsterDieBonus(monster);

    -- 检查是否有观众需要替换图片
    checkAudienceIcon();

    -- 怪物
    if monster:isAudience() then
        local pos = args.pos;
        local classId = monster:getClassId();

        if not talkConfigByClass[classId] then
            -- 不是关注的怪物
            return;
        end

        if table.indexOf(deadAudienceTempList, pos) == -1 then
            table.insert(deadAudienceTempList, pos);
        end
    end
end

-- 怪物死亡奖励（直接奖励，非掉落）
function monsterDieBonus(monster)
    if not monster or not monster:isAudience() then
        return;
    end

    local classId = monster:getClassId();
    local dbase = queryAudience(classId, "dbase");
    local bonus = dbase["die_bonus"];
    if type(bonus) == "string" then
        -- 配置了公式计算
        local args = queryAudience(classId, "dbase");
        bonus = FormulaM.invoke(bonus, ME.user, args);
    end

    if type(bonus) ~= "table" or #bonus <= 0 then
        return;
    end

    if type(bonus[1]) == "number" then
        -- 奖励
        BonusM.doBonus(bonus, "audience_die_bonus");
    elseif type(bonus[1]) == "table" then
        for _, info in ipairs(bonus) do
            -- 奖励
            BonusM.doBonus(info, "audience_die_bonus");
        end
    end

    -- 抛出事件
    EventMgr.fire(event.AUDIENCE_DIE_BONUS, {["pos"] = monster:getPos(), ["bonus"] = bonus});
end

-- 观众替换图片事件
function checkAudienceIcon()
    local audiences = getAliveAudience();
    for _, pos in pairs(audiences) do
        local grid = DungeonM.getGridByPos(pos);
        local monster = grid.monster;
        local classId = monster.classId;

        local dbase = queryAudience(classId, "dbase");
        local invincible = dbase["invincible"];
        if invincible and not isAudienceInvincible(monster) and dbase["change_icon"] then
            -- 原本无敌的，现在不是了，抛出替换图片
            EventMgr.fire(event.AUDIENCE_CHANGE_ICON, {["pos"] = pos, ["icon"] = dbase["change_icon"]});
        end
    end
end

-- 怪物自身受创的回调
whenMonsterDamaged = function(args)
    if not isInForcus() then
        return;
    end

    local pos     = args.pos;
    local skillId = args.skillId;

    --  if type(skillId) ~= "number" or skillId <= 0 then
    --     -- 不是被技能所伤
    --     return;
    -- end

    local grid = DungeonM.getGridByPos(pos);
    if not grid then
        -- 找到格子
        return;
    end

    -- 怪物
    local monster = grid.monster;
    if monster and monster:isAudience() then
        local classId = monster.classId;
        if table.indexOf(damagedAudienceTempList, pos) == -1 then
            table.insert(damagedAudienceTempList, pos);
        end

        if hasWon() then
            hasWinTalk = true;
        end
    end
end

-- 定时检测观众发言请求
checkAudienceTalk = function()
    if not isInForcus() then
        -- 已经不在场景了
        return;
    end

    -- 如果发言队列不为空，直接响应请求
    if #audienceTalkQueue > 0 then
        execTalkQueue();
        return;
    end

    -- 发言优先级：入场 > 死亡 > 胜利 > 受创 > 战斗过程
    if CombatM.getRound() <= 1 then
        -- 玩家还没起手，准备入场发言
        prepareEnterTalk();
    elseif #deadAudienceTempList > 0 then
        -- 死亡队不为空，准备死亡发言
        prepareDeadTalk();
    elseif hasWon() then
        -- 玩家已经胜利了，准备胜利发言
        prepareWinTalk();
    elseif #damagedAudienceTempList > 0 then
        -- 受创队列不为空，准备受创发言
        prepareDamagedTalk();
    else
        -- 准备战斗过程中的发言
        prepareCombatTalk();
    end
end

-- 响应发言队列中的请求
function execTalkQueue()
    if #audienceTalkQueue == 0 then
        return;
    end

    -- 如果队列中有发言请求，逐个触发
    for _, item in pairs(audienceTalkQueue) do
        local pos   = item.pos;
        local delay = item.delay;
        local content = item.content;
        local talkType = item.talkType;
        triggerAudienceTalk(pos, content, delay, talkType);
    end

    audienceTalkQueue = {};
end

-- 入场发言
function prepareEnterTalk()
    local curTime = os.time();
    if curTime - lastEnterTalkTime < STAGE_TALK_INTERVAL then
        -- 入场发言冷却中
        return;
    end

    -- 先清空发言队列
    audienceTalkQueue = {};

    -- 记录最近一次入场发言的时间
    lastEnterTalkTime = os.time();

    -- 找到场上所有存活的观众/或者是需要说话的怪物
    local audiences = getAliveAudience();
    for _, pos in pairs(audiences) do
        local grid = DungeonM.getGridByPos(pos);
        local monster = grid.monster;
        local classId = monster.classId;

        local content = getContent(classId, AUDIENCE_TALK_STAGE_ENTER);
        if content then
            -- 随机延时一下，错开发言时间
            local delay = math.random(1, 10)/10;
            local item = { ["pos"] = pos, ["content"] = content, ["delay"] = delay};
            table.insert(audienceTalkQueue, item);
        end
    end

    -- 部分怪物需要入场发言
    local monsters = getAliveTalkMonster();
    if #monsters > MAX_MONSTER_NUM then
        monsters = randomFetch(monsters, MAX_MONSTER_NUM);
    end

    if #monsters < 0 then
        return;
    end

    local talks = {};
    for index, pos in pairs(monsters) do
        local grid = DungeonM.getGridByPos(pos);
        local monster = grid.monster;
        local classId = monster.classId;
        local baseId = MonsterM.query(classId, "base_class_id");

        -- 抽一组不重复的对话
        if not talks[baseId] then
            local contents = getAllContent(baseId, AUDIENCE_TALK_STAGE_ENTER);
            talks[baseId] = randomFetch(contents, MAX_MONSTER_NUM);
        end

        local content = talks[baseId][index];
        if content then
            -- 随机延时一下，错开发言时间
            local delay = math.random(1, 10)/10;
            local item = { ["pos"] = pos, ["content"] = content, ["delay"] = delay, ["talkType"] = "special_monster"};
            table.insert(audienceTalkQueue, item);
        end
    end
end

-- 随机抽取指定数量
function randomFetch(list, num)
    if #list <= num then
        return list;
    end

    local arr = table.deepcopy(list);
    local ret = {};
    for i = 1, num do
        local index = math.random(#arr);
        table.insert(ret, arr[index]);
        table.remove(arr, index);
    end

    return ret;
end

-- 获取所有存活/并且不在发言冷却的怪物
function getAliveTalkMonster()
    local ret = {};
    local grids = DungeonM.getCurrentDungeon();
    for _, grid in pairs(grids) do
        repeat
            local pos = grid:getPos();
            local monster = grid.monster;
            if not monster or monster:isDead() then
                -- 没有怪物，或者怪物已经死亡
                break;
            end

            if monster.isAudience and monster:isAudience() then
                -- 观众怪物不在这边处理
                break;
            end

            local classId = monster.classId;
            local dbase = MonsterM.query(classId, "dbase") or {};
            if dbase["audience_talk"] ~= 1 then
                -- 不是关注的怪物
                break;
            end

            if isInTalkCD(pos) then
                -- 在说话冷却时间
                break;
            end

            -- 插入队列
            table.insert(ret, pos);
        until true
    end

    return ret;
end

-- 准备战斗过程中发言
function prepareCombatTalk()
    local curTime = os.time();
    if (curTime - lastCombatTalkTime) < STAGE_TALK_INTERVAL then
        -- 战斗过程发言冷却中
        return;
    end

    if curTalkLevel > COMBAT_PROCESS_TALK then
        -- 如果当前等级大于战斗等级
        return;
    end

    -- 当前对话等级
    curTalkLevel = COMBAT_PROCESS_TALK;

    -- 记录最近一次战斗发言的时间
    lastCombatTalkTime = os.time();

    -- 找到场上所有存活的观众
    local audiences = getAliveAudience();
    for _, pos in pairs(audiences) do
        local grid = DungeonM.getGridByPos(pos);
        local monster = grid.monster;
        local classId = monster.classId;

        local content = getContent(classId, AUDIENCE_TALK_STAGE_COMBAT);
        if content then
            -- 随机延时一下，错开发言时间
            local delay = math.random(1, 10)/10;
            local item = { ["pos"] = pos, ["content"] = content, ["delay"] = delay};
            table.insert(audienceTalkQueue, item);
        end
    end
end

-- 准备观众受创发言
function prepareDamagedTalk()
    if #damagedAudienceTempList == 0 then
        return;
    end

    if curTalkLevel > AUDIENCE_DAMAGE_TALK then
        -- 如果当前等级大于受创等级
        return;
    end

    -- 当前对话等级
    curTalkLevel = AUDIENCE_DAMAGE_TALK;

    -- 找到场上所有存活的观众
    local audiences = getAliveAudience();
    for _, pos in pairs(audiences) do
        local grid = DungeonM.getGridByPos(pos);
        local monster = grid.monster;
        local classId = monster.classId;

        local content = getContent(classId, AUDIENCE_TALK_STAGE_DAMAGED);
        if content then
            -- 随机延时一下，错开发言时间
            local delay = math.random(1, 10)/10;
            local item = { ["pos"] = pos, ["content"] = content, ["delay"] = delay};
            table.insert(audienceTalkQueue, item);
        end
    end
end

-- 准备观众死亡发言
function prepareDeadTalk()
    if #deadAudienceTempList == 0 then
        return;
    end

    if curTalkLevel > AUDIENCE_DIED_TALK then
        return;
    end

    curTalkLevel = AUDIENCE_DIED_TALK;

    -- 找到场上所有存活的观众
    local audiences = getAliveAudience();
    for _, pos in pairs(audiences) do
        local grid = DungeonM.getGridByPos(pos);
        local monster = grid.monster;
        local classId = monster.classId;

        local content = getContent(classId, AUDIENCE_TALK_STAGE_DIE);
        if content then
            -- 随机延时一下，错开发言时间
            local delay = math.random(1, 10)/10;
            local item = { ["pos"] = pos, ["content"] = content, ["delay"] = delay};
            table.insert(audienceTalkQueue, item);
        end
    end
end

-- 准备观众胜利发言
function prepareWinTalk()
    local curTime = os.time();
    if curTime - lastWinTalkTime < STAGE_TALK_INTERVAL then
        -- 胜利发言冷却中
        return;
    end

    if curTalkLevel > PLAYER_WIN_TALK then
        return;
    end

    curTalkLevel = PLAYER_WIN_TALK;

    -- 找到场上所有存活的观众
    local audiences = getAliveAudience();
    for _, pos in pairs(audiences) do
        local grid = DungeonM.getGridByPos(pos);
        local monster = grid.monster;
        local classId = monster.classId;

        local content = getContent(classId, AUDIENCE_TALK_STAGE_WIN);
        if content then
            -- 随机延时一下，错开发言时间
            local delay = math.random(1, 10)/10;
            local item = { ["pos"] = pos, ["content"] = content, ["delay"] = delay};
            table.insert(audienceTalkQueue, item);
        end
    end
end

-- 重置发言记录
function resetTalkRecord()
    lastEnterTalkTime = 0;
    lastCombatTalkTime = 0;
    lastWinTalkTime = 0;
    curTalkLevel = 1;
    hasWinTalk = false;
    deadAudienceTempList = {};
    damagedAudienceTempList = {};
    gridsLastTalkTimeMap = {};
end

-- 获取所有的对话
function getAllContent(classId, type)
    local ret = {};

    if not talkConfigByClass[classId] then
        return ret;
    end

    local talks = table.deepcopy(talkConfigByClass[classId][type]);
    if not talks or #talks <= 0 then
        -- 没有配置
        return ret;
    end

    for _, info in ipairs(talks) do
        local content = finalContent(info["content"]);
        table.insert(ret, content);
    end

    return ret;
end

-- 获取对话内容
function getContent(classId, type)
    if not talkConfigByClass[classId] then
        return;
    end

    local talks = table.deepcopy(talkConfigByClass[classId][type]);
    if not talks or #talks <= 0 then
        -- 没有配置
        return;
    end

    local index = math.random(#talks);
    local info = talks[index];
    local content = finalContent(info["content"]);

    return content;
end

-- 拼凑部分对话
function finalContent(content)
    -- 动态获取玩家史莱姆名称
    if string.match(content, "{user_slime}") ~= nil then
        local userSlime = ME.user:getActivePet();
        content = string.gsub(content, "{user_slime}", PetM.query(userSlime.classId, "name"));
    end

    return content;
end

-- 获取所有存活/并且不在发言冷却的观众
function getAliveAudience()
    local ret = {};
    local grids = DungeonM.getCurrentDungeon();
    for _, grid in pairs(grids) do
        local monster = grid.monster;
        local pos = grid:getPos();
        if monster and not monster:isDead() and monster:isAudience()
            and not isInTalkCD(pos) then
            table.insert(ret, pos);
        end
    end

    return ret;
end

-- 是否在关注的楼层
function isInForcus()
    -- TODO 暂时只有活动副本BOSS层
    return ActivityDungeonM.isInActivityBossLayer();
end

-- 是否在发言冷却中
function isInTalkCD(pos)
    local lastTalkTime = gridsLastTalkTimeMap[pos] or 0;
    if type(lastTalkTime) ~= "number" then
        return false;
    end

    if os.time() - lastTalkTime >= GRID_TALK_CD_TIME then
        return false;
    end

    return true;
end

-- 触发观众发言
-- @param pos     格子位置
-- @param content 发言内容
-- @param delay   延时几秒后播放
function triggerAudienceTalk(pos, content, delay, talkType)
    delay = delay or 0;

    -- 记录该格子最近一次发言时间
    gridsLastTalkTimeMap[pos] = os.time() + delay;

    -- 抛出发言事件
    EventMgr.fire(event.AUDIENCE_TALK, { ["pos"] = pos, ["content"] = content, ["delay"] = delay, ["talkType"] = talkType});
end

-- 当前是否胜利了
function hasWon()
    if not isInForcus() then
        return false;
    end

    local grids = DungeonM.getCurrentDungeon();
    for _, grid in pairs(grids) do
        local monster = grid.monster;
        local pos = grid:getPos();
        if monster and not monster:isDead() and not monster:isAudience() then
            -- 存在不是观众的怪物
            return false;
        end
    end

    return true;
end

-- 计算观众掉落
function calcDrop(monster)
    local classId = monster.classId;
    if monster:isAudience() then
        -- 观众怪
        local bonus = queryAudience(classId, "drop_bonus");
        if type(bonus) == "table" and #bonus > 0 then
            -- 配置了固定掉落
            return bonus;
        elseif type(bonus) == "string" then
            -- 配置了公式计算
            local args = queryAudience(classId, "dbase");
            return FormulaM.invoke(bonus, ME.user, args);
        end
    end

    return nil;
end

-- 初始化怪物的回调
initMonster = function(monster)
    if not isInForcus() then
        return false;
    end

    local classId = monster:getClassId();
    local formualName = queryAudience(classId, "init_monster");
    if type(formualName) == "string" and formualName ~= "" then
        -- 配置了初始化公式
        local args = queryAudience(classId, "init_args");

        -- 计算初始化参数
        local dbase = FormulaM.invoke(formualName, ME.user, monster, args);
        monster.dbase:aborb(dbase);

        if dbase.action then
            monster.action = dbase.action;
        end
    end
end

-- 当前场上只剩下自己
function leaveMonsterAlone(monster)
    local grids = DungeonM.getCurrentDungeon();
    for _, grid in pairs(grids) do
        -- 没死的怪物才需要处理
        if grid:getPos() ~= monster:getPos()
            and grid:isMonster() and
            not grid.monster:isDead() then
            return false;
        end
    end

    return true;
end

-- 观众当前是否无敌
function isAudienceInvincible(monster)
    if not monster or monster.type ~= OBJECT_TYPE_MONSTER then
        return false;
    end

    if not isInForcus() then
        return false;
    end

    -- 判断是否是观众
    if not monster or not monster:isAudience() then
        return false;
    end

    local ret = false;
    local classId = monster.classId;
    local dbase = queryAudience(classId, "dbase") or {};
    if type(dbase["invincible"]) == "table" then
        for key, value in pairs(dbase["invincible"]) do
            if key == "not_alone" and not leaveMonsterAlone(monster) then
                -- 当前还有别的怪物存在，无敌
                ret = true;
                break;
            end
        end
    end

    return ret;
end

-- 是否需要调整
function needFixPosition()
    if UnrealDungeonM.isInUnreal() then
        -- TODO 暂时认为夹层中不调整
        return false;
    end

    return true;
end

-- 这个观众是否允许时间停止
function allowStopAction(monster)
    -- 观众改为都会受到时间停止影响
    do return true end;

    if not monster.isAudience or not monster:isAudience() then
        -- 不是观众，可以
        return true;
    end

    -- 是观众，只有配置了可以，才允许时间停止
    local classId = monster.classId;
    local dbase = queryAudience(classId, "dbase") or {};
    return dbase["allow_stop_time"] == 1;
end
